""" Module that defines AttributeModel
"""
from __future__ import absolute_import

__all__ = ['browse', 'execute', 'create_object_browser', 'logging_basic_config']

from objbrowser.qtpy.QtCore import Qt
from objbrowser.qtpy.QtGui import QTextOption

import logging, inspect, string, pprint, six

try:
    import numpy as np
except ImportError:
    _NUMPY_INSTALLED = False
else:
    _NUMPY_INSTALLED = True

logger = logging.getLogger(__name__)


SMALL_COL_WIDTH = 120
MEDIUM_COL_WIDTH = 200

_PRETTY_PRINTER = pprint.PrettyPrinter(indent=4)

_ALL_PREDICATES = (inspect.ismodule, inspect.isclass, inspect.ismethod,
                   inspect.isfunction, inspect.isgeneratorfunction, inspect.isgenerator,
                   inspect.istraceback, inspect.isframe, inspect.iscode,
                   inspect.isbuiltin, inspect.isroutine, inspect.isabstract,
                   inspect.ismethoddescriptor, inspect.isdatadescriptor, 
                   inspect.isgetsetdescriptor, inspect.ismemberdescriptor) 

# The cast to int is necessary to avoid a bug in PySide, See:
# https://bugreports.qt-project.org/browse/PYSIDE-20
ALIGN_LEFT  = int(Qt.AlignVCenter | Qt.AlignLeft)
ALIGN_RIGHT = int(Qt.AlignVCenter | Qt.AlignRight)

class AttributeModel(object):
    """ Determines how an object attribute is rendered in a table column or details pane
    """ 
    def __init__(self, name,
                 doc = "<no help available>",  
                 data_fn = None,  
                 col_visible = True, 
                 width = SMALL_COL_WIDTH,
                 alignment = ALIGN_LEFT, 
                 line_wrap = QTextOption.NoWrap):
        """
            Constructor
            
            :param name: name used to describe the attribute
            :type name: string
            :param doc: short string documenting the attribute
            :type doc: string
            :param data_fn: function that calculates the value shown in the UI
            :type  data_fn: function(TreeItem_ to string.
            :param col_visible: if True, the attribute is col_visible by default in the table
            :type col_visible: bool
            :param width: default width in the attribute table
            :type with: int
            :param alignment: alignment of the value in the table
            :type alignment: Qt.AlignmentFlag 
            :param line_wrap: Line wrap mode of the attribute in the details pane
            :type line_wrap: QtGui.QPlainTextEdit
        """

        if not callable(data_fn):
            raise ValueError("data_fn must be function(TreeItem)->string")
            
        self.name = name
        self.doc = doc
        self.data_fn = data_fn
        self.col_visible = col_visible
        self.width = width
        self.alignment = alignment
        self.line_wrap = line_wrap
        
    def __repr__(self):
        """ String representation """
        return "<AttributeModel for {!r}>".format(self.name)
        
    
    @property
    def settings_name(self):
        """ The name where spaces are replaced by underscores 
        """
        sname = self.name.replace(' ', '_')
        return sname.translate(None, string.punctuation).translate(None, string.whitespace)


###################
# Data functions ##
###################


def tio_call(obj_fn, tree_item):
    """ Calls obj_fn(tree_item.obj)
    """
    return obj_fn(tree_item.obj)


def safe_tio_call(obj_fn, tree_item, log_exceptions=False):
    """ Call the obj_fn(tree_item.obj). 
        Returns empty string in case of an error.
    """ 
    tio = tree_item.obj
    try:
        return str(obj_fn(tio))
    except Exception as ex:
        if log_exceptions:
            logger.exception(ex)
        return ""    


def safe_data_fn(obj_fn, log_exceptions=False):
    """ Creates a function that returns an empty string in case of an exception.
        
        :param fnobj_fn: function that will be wrapped
        :type obj_fn: object to basestring function
        :returns: function that can be used as AttributeModel data_fn attribute
        :rtype: objbrowser.treeitem.TreeItem to string function 
    """
    def data_fn(tree_item):
        """ Call the obj_fn(tree_item.obj). 
            Returns empty string in case of an error
        """ 
        return safe_tio_call(obj_fn, tree_item, log_exceptions=log_exceptions)
    
    return data_fn


def tio_predicates(tree_item):
    """ Returns the inspect module predicates that are true for this object
    """
    tio = tree_item.obj
    predicates = [pred.__name__ for pred in _ALL_PREDICATES if pred(tio)]
    return ", ".join(predicates)


def tio_summary(tree_item):
    """ Returns a small summary of regular objects. 
        For callables and modules an empty string is returned.
    """
    tio = tree_item.obj
    if isinstance(tio, six.string_types):
        return tio
    elif isinstance(tio, (list, tuple, set, frozenset, dict)):  
        n_items = len(tio)
        if n_items == 0:
            return "empty {}".format(type(tio).__name__)
        if n_items == 1:
            return "{} of {} item".format(type(tio).__name__, n_items)
        else:
            return "{} of {} items".format(type(tio).__name__, n_items)
    elif _NUMPY_INSTALLED and isinstance(tio, np.ndarray):
        return "array of {}, shape: {}".format(tio.dtype, tio.shape)
    elif callable(tio) or inspect.ismodule(tio):
        return "" 
    else:
        return str(tio)
    
    
def tio_is_attribute(tree_item):
    """ Returns 'True' if the tree item object is an attribute of the parent 
        opposed to e.g. a list element.
    """
    if tree_item.is_attribute is None:
        return ''
    else:
        return str(tree_item.is_attribute)
   
    
def tio_is_callable(tree_item):
    "Returns 'True' if the tree item object is callable"
    return str(callable(tree_item.obj)) # Python 2
    #return str(hasattr(tree_item.obj, "__call__")) # Python 3?


def tio_doc_str(tree_item):
    """ Returns the doc string of an object
    """
    tio = tree_item.obj
    try:
        return tio.__doc__
    except AttributeError:
        return '<no doc string found>'
          

#######################
# Column definitions ##
#######################

ATTR_MODEL_NAME = AttributeModel('name', 
    doc         = "The name of the object.", 
    data_fn     = lambda tree_item: tree_item.obj_name if tree_item.obj_name else '<root>',
    col_visible = True,  
    width       = SMALL_COL_WIDTH) 

ATTR_MODEL_PATH = AttributeModel('path', 
    doc         = "A path to the data: e.g. var[1]['a'].item", 
    data_fn     = lambda tree_item: tree_item.obj_path if tree_item.obj_path else '<root>', 
    col_visible = True,  
    width       = MEDIUM_COL_WIDTH) 

ATTR_MODEL_SUMMARY = AttributeModel('summary', 
    doc         = """A summary of the object for regular objects (is empty for non-regular objects
                     such as callables or modules).
                  """,
    data_fn     = tio_summary,
    col_visible = True,  
    alignment   = ALIGN_LEFT,
    width       = MEDIUM_COL_WIDTH) 

ATTR_MODEL_UNICODE = AttributeModel('unicode', 
    doc         = """The unicode representation of the object. In Python 2 it uses unicode()
                     In Python 3 the str() function is used.
                  """, 
    data_fn     = lambda tree_item: six.text_type(tree_item.obj),
    col_visible = True,  
    width       = MEDIUM_COL_WIDTH, 
    line_wrap   = QTextOption.WrapAtWordBoundaryOrAnywhere) 


ATTR_MODEL_STR = AttributeModel('str', 
    doc         = """The string representation of the object using the str() function.
                     In Python 3 there is no difference with the 'unicode' column.
                  """,
    data_fn     = lambda tree_item: str(tree_item.obj),
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH, 
    line_wrap   = QTextOption.WrapAtWordBoundaryOrAnywhere) 
 
ATTR_MODEL_REPR = AttributeModel('repr', 
    doc         = "The string representation of the object using the repr() function.", 
    data_fn     = lambda tree_item: repr(tree_item.obj),         
    col_visible = True,  
    width       = MEDIUM_COL_WIDTH, 
    line_wrap   = QTextOption.WrapAtWordBoundaryOrAnywhere) 

ATTR_MODEL_TYPE = AttributeModel('type', 
    doc         = "Type of the object determined using the builtin type() function", 
    data_fn     = lambda tree_item: str(type(tree_item.obj)),
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH) 

ATTR_MODEL_CLASS = AttributeModel('type name', 
    doc         = "The name of the class of the object via obj.__class__.__name__", 
    data_fn     = lambda tree_item: type(tree_item.obj).__name__,
    col_visible = True,  
    width       = MEDIUM_COL_WIDTH) 

ATTR_MODEL_LENGTH = AttributeModel('length', 
    doc         = "The length of the object using the len() function", 
    #data_fn     = tio_length,
    data_fn      = safe_data_fn(len),  
    col_visible = False,  
    alignment   = ALIGN_RIGHT,
    width       = SMALL_COL_WIDTH) 

ATTR_MODEL_ID = AttributeModel('id', 
    doc         = "The identifier of the object with calculated using the id() function", 
    data_fn     = lambda tree_item: "0x{:X}".format(id(tree_item.obj)), 
    col_visible = False, 
    alignment   = ALIGN_RIGHT, 
    width       = SMALL_COL_WIDTH) 

ATTR_MODEL_IS_ATTRIBUTE = AttributeModel('is attribute', 
    doc         = """The object is an attribute of the parent (opposed to e.g. a list element).
                     Attributes are displayed in italics in the table.
                  """,
    data_fn     = tio_is_attribute, 
    col_visible = False,  
    width       = SMALL_COL_WIDTH) 

ATTR_MODEL_CALLABLE = AttributeModel('is callable', 
    doc         = """True if the object is callable.
                     Determined with the `callable` built-in function.
                     Callable objects are displayed in blue in the table.
                  """,
    data_fn     = tio_is_callable, 
    col_visible = True,  
    width       = SMALL_COL_WIDTH) 

ATTR_MODEL_IS_ROUTINE = AttributeModel('is routine', 
    doc         = """True if the object is a user-defined or built-in function or method.
                     Determined with the inspect.isroutine() method.
                  """ ,
    data_fn     = lambda tree_item: str(inspect.isroutine(tree_item.obj)), 
    col_visible = False,  
    width       = SMALL_COL_WIDTH) 

ATTR_MODEL_PRED = AttributeModel('inspect predicates', 
    doc         = "Predicates from the inspect module" ,
    data_fn     = tio_predicates, 
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH) 

ATTR_MODEL_PRETTY_PRINT = AttributeModel('pretty print', 
    doc         = "Pretty printed representation of the object using the pprint module.", 
    data_fn     = lambda tree_item: _PRETTY_PRINTER.pformat(tree_item.obj),         
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH) 
        
ATTR_MODEL_DOC_STRING = AttributeModel('doc string', 
    doc         = "The object's doc string", 
    data_fn     = tio_doc_str,         
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH)
        
ATTR_MODEL_GET_DOC = AttributeModel('inspect.getdoc', 
    doc         = "The object's doc string, leaned up by inspect.getdoc()",
    data_fn     = safe_data_fn(inspect.getdoc),         
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH)
        
ATTR_MODEL_GET_COMMENTS = AttributeModel('inspect.getcomments', 
    doc         = "Comments above the object's definition. Retrieved using inspect.getcomments()",
    data_fn     = lambda tree_item: inspect.getcomments(tree_item.obj),         
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH)
        
ATTR_MODEL_GET_MODULE = AttributeModel('inspect.getmodule', 
    doc         = "The object's module. Retrieved using inspect.module",
    data_fn     = safe_data_fn(inspect.getmodule),         
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH) 
        
ATTR_MODEL_GET_FILE = AttributeModel('inspect.getfile', 
    doc         = "The object's file. Retrieved using inspect.getfile",
    data_fn     = safe_data_fn(inspect.getfile),         
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH)
        
ATTR_MODEL_GET_SOURCE_FILE = AttributeModel('inspect.getsourcefile', # calls inspect.getfile()
    doc         = "The object's file. Retrieved using inspect.getsourcefile",
    data_fn     = safe_data_fn(inspect.getsourcefile),         
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH)
        
ATTR_MODEL_GET_SOURCE_LINES = AttributeModel('inspect.getsourcelines', 
    doc         = "Uses inspect.getsourcelines() to get a list of source lines for the object", 
    data_fn     = safe_data_fn(inspect.getsourcelines),         
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH)
        
ATTR_MODEL_GET_SOURCE = AttributeModel('inspect.getsource', 
    doc         = "The source code of an object retrieved using inspect.getsource", 
    data_fn     = safe_data_fn(inspect.getsource),         
    col_visible = False,  
    width       = MEDIUM_COL_WIDTH) 
        

ALL_ATTR_MODELS = (
    ATTR_MODEL_NAME,
    ATTR_MODEL_PATH, 
    ATTR_MODEL_SUMMARY,
    ATTR_MODEL_UNICODE, 
    ATTR_MODEL_STR, 
    ATTR_MODEL_REPR,    
    ATTR_MODEL_TYPE, 
    ATTR_MODEL_CLASS, 
    ATTR_MODEL_LENGTH, 
    ATTR_MODEL_ID, 
    ATTR_MODEL_IS_ATTRIBUTE, 
    ATTR_MODEL_CALLABLE, 
    ATTR_MODEL_IS_ROUTINE,     
    ATTR_MODEL_PRED,
    ATTR_MODEL_PRETTY_PRINT,
    ATTR_MODEL_DOC_STRING, 
    ATTR_MODEL_GET_DOC, 
    ATTR_MODEL_GET_COMMENTS, 
    ATTR_MODEL_GET_MODULE, 
    ATTR_MODEL_GET_FILE, 
    ATTR_MODEL_GET_SOURCE_FILE, 
    ATTR_MODEL_GET_SOURCE_LINES, 
    ATTR_MODEL_GET_SOURCE)


DEFAULT_ATTR_COLS = (
    ATTR_MODEL_NAME,
    ATTR_MODEL_PATH, 
    ATTR_MODEL_SUMMARY,
    ATTR_MODEL_UNICODE, 
    ATTR_MODEL_STR, 
    ATTR_MODEL_REPR,    
    ATTR_MODEL_LENGTH, 
    ATTR_MODEL_TYPE, 
    ATTR_MODEL_CLASS, 
    ATTR_MODEL_ID, 
    ATTR_MODEL_IS_ATTRIBUTE,     
    ATTR_MODEL_CALLABLE, 
    ATTR_MODEL_IS_ROUTINE,     
    ATTR_MODEL_PRED,    
    ATTR_MODEL_GET_MODULE, 
    ATTR_MODEL_GET_FILE, 
    ATTR_MODEL_GET_SOURCE_FILE)

DEFAULT_ATTR_DETAILS = (
    ATTR_MODEL_PATH, # to allow for copy/paste  
    #ATTR_MODEL_SUMMARY, # Too similar to unicode column
    ATTR_MODEL_UNICODE, 
    #ATTR_MODEL_STR, # Too similar to unicode column
    ATTR_MODEL_REPR,
    ATTR_MODEL_PRETTY_PRINT,
    #ATTR_MODEL_DOC_STRING, # not used, too similar to ATTR_MODEL_GET_DOC
    ATTR_MODEL_GET_DOC, 
    ATTR_MODEL_GET_COMMENTS, 
    #ATTR_MODEL_GET_MODULE, # not used, already in table 
    ATTR_MODEL_GET_FILE,         
    #ATTR_MODEL_GET_SOURCE_FILE,  # not used, already in table 
    #ATTR_MODEL_GET_SOURCE_LINES, # not used, ATTR_MODEL_GET_SOURCE is better
    ATTR_MODEL_GET_SOURCE)

# Sanity check for duplicates
assert len(ALL_ATTR_MODELS) == len(set(ALL_ATTR_MODELS))
assert len(DEFAULT_ATTR_COLS) == len(set(DEFAULT_ATTR_COLS))
assert len(DEFAULT_ATTR_DETAILS) == len(set(DEFAULT_ATTR_DETAILS))



